// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SerializedFileWriter.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Threading;
    using System.Xml;
    using Microsoft.Robotics.IO;

    /// <summary>
    /// A weakly-typed forwarder that serializes messages and saves to disk.
    /// </summary>
    internal class SerializedFileWriter : IDisposable
    {
        /// <summary>
        /// The time when recording started across all instances of FileWriter. This is used in calculating the offset of each message from the global time origin.
        /// </summary>
        private static long universalStartTime;

        /// <summary>
        /// The data contract serializer to use
        /// </summary>
        private DataContractSerializer serializer;

        /// <summary>
        /// The file path to write and read 
        /// </summary>
        private string filePath;

        /// <summary>
        /// The FileStream to open and write 
        /// </summary>
        private FileStream fileStream;

        /// <summary>
        /// Xml Writer
        /// </summary>
        private XmlWriter writer;

        /// <summary>
        /// A synchronization object.
        /// </summary>
        private object syncRoot;

        /// <summary>
        /// The type of xml serialization. True for binary, false for xml.
        /// </summary>
        private bool binarySerialization;

        /// <summary>
        /// Initializes a new instance of the SerializedFileWriter class
        /// </summary>
        /// <param name="types">The types of messages that will be saved to disk using this writer</param>
        /// <param name="filePath">The file path</param>
        /// <param name="agentName">The name of the agent generating the data. This is used in generating a file name if one is not specified.</param>
        /// <param name="binarySerialization">The value indicating whether to apply binary serialization or xml serialization.</param>
        /// <param name="enableArrayCompression">If true, array compression is enabled. This trades off CPU utilization (higher) for file size (smaller) .</param>
        public SerializedFileWriter(IEnumerable<Type> types, string filePath, string agentName, bool binarySerialization, bool enableArrayCompression)
        {
            this.filePath = filePath;
            this.binarySerialization = binarySerialization;
            this.serializer = new DataContractSerializer(
                typeof(AgentMessage),
                types,
                2048, // TODO: reduce back to 256 - Bug 1836 Add Pose2D to SerailableArray list
                true,
                false,
                new SerializationSurrogate(!enableArrayCompression));
            this.syncRoot = new object();

            // if the file path is empty or is just a directory, generate a file name. 
            // Make sure all filenames that are part of the same recording have the same tick component 
            Interlocked.CompareExchange(ref universalStartTime, DateTime.Now.Ticks, 0);
            string fileName = string.Format("{0}_{1}.xml", universalStartTime.ToString(), agentName);
            if (this.filePath == null)
            {
                this.filePath = fileName;
            }
            else if (!Path.HasExtension(this.filePath))
            {
                this.filePath = Path.Combine(this.filePath, fileName);
            }

            this.fileStream = new FileStream(this.filePath, FileMode.Create, FileAccess.Write);

            if (this.binarySerialization)
            {
                this.writer = XmlDictionaryWriter.CreateBinaryWriter(this.fileStream, null, null, false);
            }
            else
            {
                XmlWriterSettings writerSettings = new XmlWriterSettings();
                writerSettings.OmitXmlDeclaration = true;
                writerSettings.Indent = false;
                writerSettings.NewLineHandling = NewLineHandling.None;
                writerSettings.NamespaceHandling = NamespaceHandling.OmitDuplicates;
                this.writer = XmlWriter.Create(this.fileStream, writerSettings);
            }

            this.writer.WriteStartElement("root");
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="SerializedFileWriter"/> class.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        /// <summary>
        /// Accepts a message, serializes, and saves to disk
        /// </summary>
        /// <param name="message">Message to write</param>
        public void Write(object message)
        {
            // take a lock to make sure that messages of different types are sequenced and that the file stream is not closed. 
            // The delay introduced by taking a lock is not an issue here, since the messages are already timestamped.
            lock (this.syncRoot)
            {                
                if (this.writer != null)
                {                                      
                    // serialize the message and save to the already-opened file
                    this.serializer.WriteObject(this.writer, message);                    
                }
            }
        }

        /// <summary>
        /// Stop writing messages and write the end element so that the XML is well formed.
        /// </summary>
        public void Stop()
        {
            lock (this.syncRoot)
            {
                if (this.fileStream != null)
                {
                    this.writer.WriteEndElement();
                    this.writer.Close();
                    this.writer = null;
                    this.fileStream.Close();                    
                    this.fileStream = null;
                }
            }
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="SerializedFileWriter"/> class.
        /// </summary>
        /// <param name="disposing">True if disposing, false if finalizing</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.fileStream != null)
                {
                    this.fileStream.Dispose();
                }

                GC.SuppressFinalize(this);
            }
        }
    }
}
